﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace NumbersToEnglish.Engine
{
    /// <summary>
    /// NumbersToEnglish main class
    /// </summary>
    /// <remarks>
    /// This does all the work to convert numbers (in three digit splits) into words
    /// </remarks>
    /// <author>Ferdeen Mughal</author>
    /// <version>$Id: NumbersToEnglish.cs,v 1.0 13/12/2013</version>
    public class NumbersToEnglish : NumbersToEnglishBase, INumbersToEnglish
    {
        public string ToEnglish
        {
            get { return _ToEnglish; }
        }

        public NumbersToEnglish(Int64 number)
        {
            _Number = number;
        }

        /// <summary>
        /// Split the number into three digit groups.
        /// </summary>
        public NumbersToEnglish CreateThreeDigitGroups()
        {
            return ExecuteFaultHandledOperation(() =>
            {
                int[] groups = SplitIntoThousands(_Number).ToArray();

                _ThreeDigitGroups = groups;

                return this;
            });
        }

        /// <summary>
        /// Convert each three digit group into words.
        /// Each group runs in parallel.
        /// </summary>
        public NumbersToEnglish ConvertThreeDigitGroupsIntoWords()
        {
            return ExecuteFaultHandledOperation(() =>
            {
                if (_ThreeDigitGroups == null) return this;

                string[] groupText = new string[_ThreeDigitGroups.Length];

                Parallel.For(0, _ThreeDigitGroups.Length, i =>
                {
                    groupText[i] = ThreeDigitGroupToWords(_ThreeDigitGroups[i]);
                });

                _GroupText = groupText;

                return this;
            });
        }

        /// <summary>
        /// Combine the three digit groups
        /// </summary>
        public NumbersToEnglish CombineGroups()
        {
            return ExecuteFaultHandledOperation(() =>
            {
                if (_GroupText == null) return this;
                if (_ThreeDigitGroups == null) return this;

                string combined = _GroupText[0];

                // determine if an 'and' is needed
                bool appendAnd = (_ThreeDigitGroups[0] > 0) && (_ThreeDigitGroups[0] < 100);

                // process groups, smallest to largest
                for (int i = 1; i < _ThreeDigitGroups.Length; i++)
                {
                    // only add non-zero items
                    if (_ThreeDigitGroups[i] == 0) continue;

                    // build the string to add as a prefix
                    string prefix = string.Format("{0} {1}", _GroupText[i], NumbersMap.Scale[i]);

                    if (combined.Length != 0)
                    {
                        prefix += appendAnd ? " and " : ", ";
                    }

                    // reset 'and' 
                    appendAnd = false;

                    // combine three-digit group
                    combined = string.Format("{0}{1}", prefix, combined);
                }

                _ToEnglish = combined;

                return this;
            });
        }

        /// <summary>
        /// This will go through three digit group and covert to words
        /// </summary>
        /// <param name="threeDigits">Thre digit groups</param>
        private string ThreeDigitGroupToWords(int threeDigits)
        {
            return ExecuteFaultHandledOperation(() =>
            {
                lock (_Lock)
                {
                    string groupText = string.Empty;

                    // Determine the hundreds and the remainder
                    var numberAndRemainder = DetermineNumberAndRemainder(threeDigits, 100);

                    int hundreds = numberAndRemainder.Item1;
                    int tensUnits = numberAndRemainder.Item2;

                    // Hundreds rules
                    if (hundreds != 0)
                    {
                        groupText += string.Format("{0} Hundred", NumbersMap.Small[hundreds]);

                        if (tensUnits != 0)
                        {
                            groupText += " and ";
                        }
                    }

                    // Determine the tens and units
                    numberAndRemainder = DetermineNumberAndRemainder(tensUnits, 10);

                    int tens = numberAndRemainder.Item1;
                    int units = numberAndRemainder.Item2;

                    // Tens rules
                    if (tens >= 2)
                    {
                        groupText += NumbersMap.Tens[tens];
                        if (units != 0)
                        {
                            groupText += string.Format(" {0}", NumbersMap.Small[units]);
                        }
                    }
                    else
                    {
                        if (tensUnits != 0)
                        {
                            groupText += NumbersMap.Small[tensUnits];
                        }
                    }

                    return groupText;
                }
            });
        }

        /// <summary>
        /// Get the number part and any remainder
        /// </summary>
        /// <param name="digits"></param>
        /// /// <param name="unit"></param>
        /// <returns>Tuple of numbers and units</returns>
        private static Tuple<int, int> DetermineNumberAndRemainder(int digits, int unit)
        {
            int numbers = digits / unit;
            int units = digits % unit;

            return new Tuple<int, int>(numbers, units);
        }

        /// <summary>
        /// Split number into 1000's using Mod 1000
        /// Calling code can enumerate over the results
        /// </summary>
        /// <param name="number"></param>
        /// <returns>IEnumberalbe<int></int></returns>
        private static IEnumerable<int> SplitIntoThousands(Int64 number)
        {
            while (number != 0)
            {
                yield return (int)(number % 1000);
                number /= 1000;
            }
        }

        private readonly Int64 _Number;
        private int[] _ThreeDigitGroups;
        private string[] _GroupText;
        private string _ToEnglish;
        private readonly Object _Lock = new Object();
    }
}
